一、Android 中的 Linux 系统调用
Android 基于 Linux 内核,所有底层操作最终都通过系统调用(syscall)完成。NDK 开发中可以直接使用 POSIX API,这些 API 内部封装了系统调用:
| 类别 | 系统调用 | 函数封装 | AOSP 使用场景 |
|---|---|---|---|
| 文件 I/O | open/read/write/close |
fopen/fread/fwrite/fclose |
文件读写 |
| 进程管理 | fork/execve/waitpid |
fork/exec/wait |
Zygote 进程孵化 |
| 内存管理 | mmap/munmap/brk |
malloc/free(内部调用) |
Binder 内存映射、匿名共享内存 |
| 线程同步 | futex/clone |
pthread_create/pthread_mutex_lock |
所有线程操作 |
| 网络 I/O | socket/bind/connect/send/recv |
getaddrinfo/connect |
网络通信 |
| 设备控制 | ioctl |
OS 封装函数 | HAL 层与硬件通信 |
| 文件监控 | inotify_add_watch |
FileObserver(Java API) |
文件变化监听 |
Android 的系统调用表在 bionic/libc/kernel/uapi/asm-generic/unistd.h 中定义。32 位 ARM 架构使用 __NR_ 前缀(如 __NR_openat),通过 svc #0 指令触发;64 位 ARM 使用 svc #0 但系统调用号表不同。
二、文件 I/O:系统调用 vs C 库
Android NDK 中,文件 I/O 有两种方式:
2.1 无缓冲 I/O(系统调用)
|
open、read、write、close 是系统调用,每次调用都涉及用户态到内核态的切换,开销较大。
2.2 缓冲 I/O(C 标准库)
|
FILE* 系列函数在用户态维护了一个缓冲区(默认 8KB),减少了系统调用的次数。但当需要精确的控制(如实时性要求高的场景、需要对文件描述符进行 poll/epoll/select 操作)时,应该使用无缓冲 I/O。
2.3 实用技巧:mmap 文件映射
|
Android 的 ART 运行时加载 DEX/OAT 文件时使用 mmap 将其直接映射到进程空间,从而实现高效的代码和数据访问。Binder 驱动的数据传输也基于 mmap(一次性映射 1MB-16KB 的内核缓冲区,后续数据传输无需额外拷贝)。
源码路径:frameworks/native/libs/binder/ProcessState.cpp → mmap(NULL, BINDER_VM_SIZE, ...)
三、进程管理:Zygote 模型
Android 应用进程不是通过传统 fork+exec 创建的,而是通过 Zygote 进程 的 fork 机制孵化,这极大地加速了应用启动。
init 进程 |
Zygote 的精妙之处在于 Linux fork 的 Copy-on-Write(CoW)特性:
fork()后,子进程与父进程共享物理内存页(只读)。- 只有当子进程尝试写入某一页时,内核才会为该页创建一份副本。
- 因此,Zygote 预加载的 Framework 代码和资源在所有应用进程之间共享,大大减少了整体内存占用。
在 NDK 中,fork() 可以直接使用,但 Android 对其有限制:
|
fork() 后子进程中的注意事项:
- 只有调用
fork()的线程被复制到子进程。其它线程消失(可能导致持有的锁无法释放,造成死锁)。 - 子进程必须使用
_exit()而不是exit(),因为exit()会调用atexit注册的清理函数。 - 子进程不能安全地使用父进程的 JNIEnv(需要通过 AttachCurrentThread 重新获取)。
四、信号处理
信号(Signal)是 Linux 中进程间异步通知的机制。Android 的 tombstone(崩溃日志)就是由 debuggerd 捕获 crash 信号后生成的。
|
Android 的 debuggerd 守护进程(源码路径:system/core/debuggerd/)通过 ptrace attach 到 crash 的进程,读取寄存器、调用栈、内存映射等信息,生成 tombstone 文件(保存在 /data/tombstones/)。NDK 开发中分析 native crash 的首要工具就是 tombstone 和 ndk-stack。
信号处理器的限制:
- 只能使用异步信号安全的函数(
man 7 signal-safety)。 - 不能调用
malloc/free、printf/fprintf、pthread_mutex_lock等。 - 安全的函数主要是:
write、read、open、close、_exit、signal、raise。 - 推荐使用
SA_ONSTACK标志,为信号处理器分配独立栈空间(避免栈溢出时信号处理器无法执行)。
五、epoll:高效 I/O 多路复用
epoll 是 Linux 特有的 I/O 多路复用机制,Android 的 MessageQueue 和网络框架底层都依赖它(Java 层的 Looper → Native 层的 Looper::pollInner → epoll_wait):
|
epoll 的两种触发模式:
- 水平触发(Level Triggered):只要 fd 有可读数据,每次
epoll_wait都会返回该事件。类似poll,编程简单。 - 边缘触发(Edge Triggered):仅在 fd 状态从不可读变为可读时通知一次。需要循环
read直到返回EAGAIN,编程复杂但性能更好(减少重复通知)。
Android 的 android::Looper(Native 层)在 system/core/libutils/Looper.cpp 中,使用 epoll 监控文件描述符事件,同时支持定时器(通过 timerfd_create + epoll 实现)。
六、Unix Domain Socket
Unix Domain Socket 是同一台设备上进程间通信(IPC)的高效方式。相比 TCP/IP,它不需要经过网络协议栈,性能更高(延迟更低,吞吐更大)。
|
Android 系统服务的 IPC 通常使用 Binder,但在一些性能敏感的场景(如 SurfaceFlinger 与 App 的 BufferQueue 通信、mediaserver 的音频数据传输)会使用 Unix Domain Socket 或匿名共享内存(ashmem)。
Android 的 installd 守护进程(包管理器后台服务)通过 Unix Domain Socket 接收 pm 命令:
installd → /dev/socket/installd (Unix Domain Socket) |
七、匿名共享内存(ashmem)
Android 特有的 ashmem 驱动提供了进程间的匿名共享内存:
|
Android 的 MemoryFile(Java API)和 IMemory(Binder 接口)底层都是对 ashmem 的封装。ashmem 的特性是:内核通过引用计数跟踪共享内存的引用者,当所有引用者释放后自动回收内存。
八、CMake 构建配置
现代 Android NDK 项目使用 CMake 作为构建系统:
# CMakeLists.txt |
app 模块的 build.gradle 中配置:
android { |
九、面试常问题目
Q1: mmap 和 read/write 的区别?什么场景下用 mmap?
read/write 需要将数据从内核缓冲区拷贝到用户空间(一次数据拷贝)。mmap 将文件映射到进程的虚拟地址空间,通过缺页中断按需加载数据,不需要显式的 read/write 调用。mmap 适合:(1) 大文件的随机访问(只需访问其中一部分);(2) 多个进程共享同一文件的数据(MAP_SHARED);(3) 零拷贝的数据传输(如 Binder)。read/write 适合小文件、顺序读写。Android 的 DEX/OAT 加载使用 mmap。
Q2: epoll 的水平触发和边缘触发有什么区别?
水平触发(LT):只要 fd 仍有未处理的数据,每次 epoll_wait 都会返回该 fd 的事件。编程简单,与 select/poll 行为一致。边缘触发(ET):只在 fd 状态变化时(如从不可读变为可读)通知一次。必须循环读取直到返回 EAGAIN,否则可能丢失事件。ET 模式的优点是可以避免重复通知,减少 epoll_wait 的调用次数,适合高并发场景。Android Native Looper 默认使用 LT 模式。
Q3: Zygote fork 为什么比直接创建进程快?
Zygote 进程在启动时预加载了 Framework 类、JNI 库、系统资源(主题、字体等)。fork 使用 Copy-on-Write 机制,子进程共享这些预加载的资源,不需要重新加载。直接创建进程(fork + exec)需要一个全新的内存空间初始化过程,要重新执行所有加载步骤。Zygote 模式下,应用启动只需 fork(复制页表,约几毫秒),然后在新进程中初始化自己的少量组件。
Q4: Unix Domain Socket 和 TCP/IP Socket 的区别?在 Android 中如何选择?
Unix Domain Socket 用于同一台设备上的进程间通信,不需要经过 IP 层和 TCP 协议栈,数据在内核中直接传输(不经过网络设备),延迟更低、吞吐更高。TCP Socket 用于网络通信,可跨设备。在 Android 中,同一设备上的 IPC 首选 Binder(有权限管理、引用计数等高级特性),但当需要流式数据传输(如音频流)或需要支持非 Java 进程时,Unix Domain Socket 是合适的补充方案。
参考源码路径:
- Android Looper(Native):
system/core/libutils/Looper.cpp - Zygote 进程:
frameworks/base/core/java/com/android/internal/os/ZygoteInit.java - Zygote fork 流程:
frameworks/base/core/jni/com_android_internal_os_Zygote.cpp - Binder mmap:
frameworks/native/libs/binder/ProcessState.cpp - debuggerd:
system/core/debuggerd/ - installd:
frameworks/native/cmds/installd/ - ashmem 驱动:
kernel/common/drivers/staging/android/ashmem.c - Bionic epoll:
bionic/libc/bionic/epoll_create.cpp




